19. Testy jednostkowe

Wyzwania:

  • dowiesz się, na czym polega testowanie oprogramowania,
  • nauczysz się konfigurować środowisko do testów oraz pisać testy jednostkowe,
  • zobaczysz, jak w łatwy sposób możesz przetestować aplikację reactową.

19.1. Testy jednostkowe – wprowadzenie

Wszystkie projekty JS-owe i reactowe, nad którymi do tej pory pracowaliśmy, skalowały się w dość szybkim tempie. Z modułu na moduł przybywało im złożoności i funkcjonalności, co przekładało się na kolejne linie kodu. Jak już zapewne dobrze wiesz, im bardziej skomplikowany i rozbudowany projekt, tym łatwiej o pomyłkę, a znalezienie błędu w jednym z dziesiątek plików może przyprawić o ból głowy. Do tej pory "testowaliśmy" nasze aplikacje sprawdzając w narzędziach developerskich i w terminalu, czy zwracane są poprawne wartości, a kod sformatowany jest prawidłowo. Takie testy, choć niewątpliwie pożyteczne, stają się niewystarczające wraz z rozrostem aplikacji, a ręczne sprawdzanie każdej funkcji czy komponentu robi się bardzo żmudne.

Na szczęście dla nas i na ten problem zostało wymyślone rozwiązanie – automatyczne testy jednostkowe. Mówiąc w dużym skrócie, testowanie jednostkowe to sprawdzanie, czy poszczególne fragmenty kodu działają zgodnie z oczekiwaniami. Słowo automatyczne oznacza natomiast, że testowaniem zajmuje się napisana do tego celu funkcja, która porównuje wartość, którą spodziewamy się otrzymać, z wartością, którą zwraca nasz kod.

Podsumowując, dzięki automatycznym testom jednostkowym:

  • możemy łatwo przetestować wiele przypadków – nie tylko oczekiwane scenariusze, ale też sytuacje, gdy np. do aplikacji zostaną przesłane błędne lub puste wartości,
  • mamy gwarancję, że wychwycimy ewentualne problemy spowodowane przez zmianę kodu w przyszłości,
  • szybko otrzymujemy informację, że kod jest błędny, oraz co poszło nie tak,
  • inni developerzy pracujący przy projekcie wiedzą, za co odpowiada konkretny fragment kodu, oraz jakie wyniki powinien zwracać.

Czy testy są potrzebne?

Zrozumienie sensowności pisania testów oraz umiejętność pisania sensownych testów przychodzą wraz z doświadczeniem. Na początku Twojej drogi Web Developera może Ci się wydawać, że jest to tylko przykry obowiązek. Z czasem jednak zaczniesz lepiej rozumieć ich rolę.

Przede wszystkim musisz wiedzieć, że testy mają być przydatne, czyli faktycznie sprawdzać, czy komponent działa poprawnie. Możesz myśleć o nich jak o pasach bezpieczeństwa w samochodzie: najlepiej, jeśli nigdy się nie przydadzą, ale w razie problemów zapewnią bezpieczeństwo – zarówno Twoje, jak i projektu!

Umiejętność tworzenia testów jednostkowych jest też bardzo mile widziana — albo wręcz wymagana — przez firmy, do których niedługo będziesz aplikować. Wynika to z faktu, że testy pozwalają dostarczać klientom produkty zawierające o wiele mniej błędów. Dlatego coraz więcej firm woli, aby developer wolniej realizował projekt, poświęcając część czasu pracy na solidne testowanie swojego kodu.

Możesz nawet spotkać się z podejściem Test-driven development (TDD) – jest to podejście, w którym najpierw pisze się testy, a dopiero potem funkcjonalność, którą te testy sprawdzają.

Nauka pisania testów pójdzie nam jednak sprawniej przy testowaniu już istniejących komponentów – dlatego właśnie od tego zaczniemy.

Frameworki testowe

Jak już pewnie zaczynasz się domyślać, nie musimy za każdym razem wynajdywać koła na nowo i pisać od podstaw środowiska testowego. Jako developerzy mamy do dyspozycji narzędzia, które ułatwią nam pisanie testów – tak zwane frameworki testowe. Do najpopularniejszych frameworków należą: Mocha, Jasmine, Jest, QUnit czy Tape. Mogą one różnić się nieco konfiguracją i niektórymi opcjami, natomiast generalnie wszystkie są dość podobne do siebie, więc wystarczy opanować jeden, żeby w przyszłości pracować swobodnie z innymi.

Frameworki odpowiadają za konfigurację środowiska testowego, sugerują sposób, w jaki testy powinny być pisane, niektóre dostarczają również funkcje do asercji, czyli porównywania wyniku testu z oczekiwanym rezultatem. Odpowiadają też za uruchamianie testów oraz przedstawienie wyników działania testów w terminalu. W połączeniu z innymi narzędziami potrafią też wygenerować raport na temat ilość kodu, który jest pokryty testami (tzw. coverage report), a nawet wskazać konkretne linijki kodu, które nie są przetestowane!

Pamiętaj jednak, że Twoim celem nie powinno być osiągnięcie 100% pokrycia kodu testami, tylko pisanie dobrych testów, tj. takich, które faktycznie sprawdzają, czy dana część funkcjonalności działa poprawnie.

Różne rodzaje testów

Testy jednostkowe nie są jedynym rodzajem testów, stosowanych w programowaniu. O ile w tym kursie zajmiemy się tylko testami jednostkowymi, lepiej zrozumiesz ich rolę, wiedząc, jak dzielimy testy aplikacji. W tym aspekcie mówimy o poziomach testów.

Poziom 1: testy jednostkowe

Unit testing polega na testowaniu małego fragmentu kodu. Może to być jedna funkcja czy jeden komponent. Dla przykładu, może to być pole wyszukiwania, w którym testy będą sprawdzać, czy wpisanie tekstu wywoła funkcję przekazaną w propsie changeSearchPhrase z odpowiednimi argumentami.

Na tym poziomie nie interesuje, nas, po co użytkownik wpisuje tekst w inpucie, co ma się wtedy stać z resztą aplikacji itd. Sprawdzamy tylko, czy ten komponent robi to, co powinien.

Mówimy tutaj o komponentach, ale w ten sam sposób możemy testować np. reducery, funkcje pomocnicze, etc.

Ten poziom testów wykonuje się w separacji od reszty aplikacji, co znaczy, że nasz test będzie przekazywał komponentowi jego propsy i sprawdzał, czy komponent odpowiednio je wykorzystuje. Oznacza to między innymi, że komponent nie będzie korzystał z reduksowego stanu, tylko z danych przekazanych mu w teście.

To podejście jest niezbędne, aby test nie był zależny od danych w stanie aplikacji. Pozwala również przetestować działanie komponentu dla różnych scenariuszy – np. jeśli komponent Trips otrzyma pustą listę wycieczek.

Poziom 2: testy integracyjne

Na tym poziomie sprawdzamy, jak komponenty współpracują ze sobą. To w tym wypadku sprawdzalibyśmy, czy wpisanie tekstu w polu wyszukiwania poprawnie przefiltruje listę wycieczek.

Integration testing pozwala uniknąć sytuacji, w których poszczególne komponenty działają dobrze, ale nie współpracują ze sobą poprawnie. Weźmy za przykład sytuację, w której pole wyszukiwania zapisuje wpisany tekst w stanie aplikacji jako searchPhrase, ale lista wycieczek korzysta z pola filterPhrase. Może się to łatwo zdarzyć, jeśli nad aplikacją pracuje kilku programistów. Oba komponenty działają poprawnie same w sobie, ale nie będą ze sobą współpracować.

Poziom 3: testy end-to-end

Inaczej zwane testami systemowymi, polegają na sprawdzaniu scenariuszy zachowań użytkowników. W tym podejściu testy symulują zachowania osoby korzystającej ze strony – czyli taki test mógłby np. otwierać stronę główną, klikać link Trips, klikać w pierwszą wycieczkę, wybrać wycieczkę dla 3 osób z prywatnym basenem, podać nazwisko i numer telefonu i kliknąć guzik wysłania zamówienia, a na końcu oczekiwać, że pojawi się strona z potwierdzeniem.

Dzięki temu poziomowi testów sprawdza się już nie tylko czy komponenty poprawnie działają i współpracują ze sobą, ale czy cała aplikacja funkcjonuje prawidłowo. Na tym etapie można wychwycić kolejne błędy – np. niedziałające linki do podstron czy problemy z wysyłaniem formularza.

Czym będziemy się zajmować?

Jak już wspomnieliśmy, będziemy zajmować się testami jednostkowymi – a właściwie nauką ich podstaw, bowiem jest to zagadnienie bardzo rozległe i skomplikowane. Zdobędziesz jednak umiejętności, które będą wystarczające na stanowisku Junior Web Developera.

Jeśli chodzi o testy integracyjne, jest to już nieco wyższy poziom i rzadko są wymagane na stanowisku juniorskim. Natomiast testy systemowe leżą w zakresie zupełnie innego stanowiska – testera automatycznego.

Teraz kiedy rozumiesz już rolę testów jednostkowych, możemy przejść do praktyki!

19.2. Testy komponentu prezentacyjnego

Zanim przejdziemy do pisania testów, musimy wybrać i zainstalować narzędzia służące do ich uruchamiania.

W przypadku Reacta bardzo często używaną kombinacją jest Jest i Enzyme (czytaj: dżest i enzajm). Pierwszy z nich jest frameworkiem służącym do uruchamiania testów, asercji (sprawdzania, czy wynik testu spełnia oczekiwania), oraz mockowania (o którym powiemy sobie nieco później). Natomiast Enzyme jest biblioteką, która ułatwi nam pisanie testów, w tym renderowanie komponentów, symulowanie eventów, czy przeszukiwanie symulowanego drzewa DOM.

Instalacja Jest i Enzyme

Mamy sporo pakietów do zainstalowania, ale zrobimy to za pomocą jednej komendy.

npm install -D babel-jest@24.8.0 enzyme@3.9.0 enzyme-adapter-react-16@1.12.1 identity-obj-proxy@3.0.0 jest@24.8.0 jest-css-modules@2.0.0 jest-environment-jsdom-fourteen@0.1.0 jest-resolve@24.8.0 jest-watch-typeahead@0.3.1 jest-prop-type-error@1.1.0 react-test-renderer@16.8.6

Po instalacji pakietów otwórz package.json i dodaj nowe taski w sekcji scripts:

"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage --colors"

Pierwszy z nich posłuży do jednorazowego uruchomienia wszystkich testów, drugi do ciągłego uruchamiania testów w czasie pracy nad projektem, a trzeci pokaże nam stopień pokrycia kodu przez testy.

Zanim jednak będziemy mogli z nich korzystać, musimy jeszcze dodać konfigurację dla Jest i Enzyme. Pobierz poniższą paczkę i rozpakuj zawarte w niej pliki do głównego katalogu projektu.

Pobierz paczkę plików

Ostatnia zmiana, która będzie nam potrzebna przed rozpoczęciem pisania testów, będzie w pliku .eslintrc. W sekcji env dodaj:

"jest": true

Wszystko gotowe – możemy przejść do pisania testów!

Pierwszy test

Dobrą praktyką jest trzymanie plików z testami jak najbliżej kodu, który sprawdzają. Z tego powodu testy naszych komponentów będziemy umieszczać w ich folderach. Przyjęło się, że w takim przypadku plik z testem powinien mieć nazwę pochodzącą od nazwy pliku, który jest testowany, z dodanym przyrostkiem .test.

Znajdź zatem katalog komponentu Hero i umieść w nim nowy plik o nazwie Hero.test.js z następującą zawartością:

import React from 'react';
import {shallow} from 'enzyme';
import Hero from './Hero';

describe('Component Hero', () => {
  it('should render without crashing', () => {
    const component = shallow(<Hero titleText='Lorem ipsum' />);
    expect(component).toBeTruthy();
  });
});

Zanim przejdziemy do omówienia kodu tego testu, sprawdźmy, czy test się wykonuje. W terminalu uruchom komendę:

npm run test

W efekcie powinniśmy zobaczyć w terminalu, że został wykonany test z pliku Hero.test.js z wynikiem pozytywnym.

image

Możemy w takim razie omówić kod naszego testu. Najpierw znajdziesz w nim importy Reacta, funkcji shallow z pakietu enzyme (którą omówimy za moment), oraz komponentu Hero.

Pod importami znajduje się funkcja describe, która będzie nam służyła do zgrupowania kilku testów. Tej grupie nadajemy opis "Component Hero" w pierwszym argumencie. Drugim argumentem jest funkcja strzałkowa, której zawartość będzie zawierała poszczególne testy.

Funkcja it służy do zdefiniowania pojedynczego testu, którego opis znajduje się w pierwszym argumencie – "should render without crashing". Drugim argumentem, podobnie jak w describe, jest funkcja strzałkowa, zawierająca wyrażenia testowe.

Kod testu składa się z dwóch linijek:

  1. w stałej component zapisujemy wynik funkcji shallow, która renderuje dla nas ten komponent,
  2. funkcja expect pozwala na sprawdzenie, czy otrzymany wynik (czyli wyrenderowany komponent) jest prawdziwy.

Za chwilę wyjaśnimy dokładniej shallow i expect – najpierw zobaczmy, jak zachowa się nasz test, kiedy wystąpi błąd przy renderowaniu komponentu. W tym celu usuń props titleText, czyli zmień <Hero titleText='Lorem ipsum' /> na <Hero />.

Ponownie uruchom npm run test i sprawdź, jak teraz wygląda komunikat w terminalu.

image

Tym razem test zwrócił wynik negatywny, ponieważ komponent Hero w propTypes ma ustawione, że props titleText jest wymagany (isRequired).

Wstaw z powrotem propsa titleText='Lorem ipsum' dla Hero. Zanim przejdziemy dalej, wygodniej będzie Ci pracować nad testami, jeśli zamiast komendy npm run test uruchomisz npm run test:watch. Po tej komendzie testy będą wykonywane automatycznie po wprowadzeniu zmian w plikach.

Budowanie quasi-zdań

Składnia testów może wydawać Ci się dziwna, ale to szybko się zmieni, kiedy zwrócisz uwagę, że za pomocą tego kodu piszemy niby-zdania.

Po pierwsze, opisy z describe i z it tworzą wspólnie zdanie "Component Hero should render without crashing". Co ciekawsze, możesz też przeczytać samą linijkę z it, czyli "it should render without crashing".

Podobnie przy expect, możesz przeczytać "Expect component to be truthy". Jeszcze lepiej to widać, gdybyśmy chcieli sprawdzić, czy wartość stałej someValue jest równa 7:

expect(someValue).toBe(7);

Składnia testów specjalnie została tak zaprojektowana, aby była zbliżona do zdań w języku angielskim. Pozwala to na prostsze zrozumienie kodu testów, a przez to oczekiwań względem komponentu czy funkcji.

Funkcja expect służy do tego, aby porównywać podany jej argument z oczekiwanym wynikiem. Przykładowo, w naszym teście, za pomocą toBeTruthy sprawdzamy, czy komponent zwraca jakąś prawdziwą wartość (fałszywe byłyby np. undefined czy 0). W dokumentacji znajdziesz wszystkie metody, które można wykorzystać na wyniku tej funkcji, podobnie jak toBeTruthy.

Testowanie brzegowe

Na razie test komponentu Hero sprawdza tylko, czy komponent renderuje się poprawnie, jeśli otrzyma props titleText. Jednak ten komponent korzysta z trzech propsów – titleText, imageSrc i variant. Dla każdego z nich chcemy sprawdzić tzw. warunki brzegowe (edge cases), czyli jak zachowa się komponent, kiedy nie podamy propsa (lub podamy błędną wartość), a jak przy jego poprawnej wartości.

W takim razie dodajmy kolejne testy – najpierw sprawdźmy, czy przy braku titleText otrzymamy błąd. Dodajmy drugi test, pod it, ale wewnątrz describe:

describe('Component Hero', () => {
  it('should render without crashing', () => {
    const component = shallow(<Hero titleText='Lorem ipsum' />);
    expect(component).toBeTruthy();
  });

  it('should throw error without required props', () => {
    expect(() => shallow(<Hero />)).toThrow();
  });
});

Również ten test będzie miał wynik pozytywny – jak już wcześniej sprawdziliśmy, wywołanie <Hero /> bez propsów wyrzuca błąd (z ang. throws an error). Stosujemy tutaj funkcję strzałkową, aby funkcja expect mogła bez zwracania błędu wykonać kod shallow(<Hero />), który powinien zwrócić błąd.

Elementy komponentu

Sprawdziliśmy już sytuacje, w których komponent powinien zadziałać albo nie zadziałać – ale nie sprawdziliśmy jeszcze, czy tytuł przekazany w propsie titleText faktycznie jest wyświetlany na stronie!

Dodajmy więc trzeci test:

it('should render correct title', () => {
  const expectedTitle = 'Lorem ipsum';
  const component = shallow(<Hero titleText={expectedTitle} />);

  const renderedTitle = component.find('.title').text();
  expect(renderedTitle).toEqual(expectedTitle);
});

Tutaj nowością są metody .find i .text, które podobnie jak shallow, dostarczane są w pakiecie Enzyme. W jego dokumentacji znajdziesz dużo więcej metod, które pozwolą zarówno na wyszukiwanie elementów i trawersowanie symulowanego drzewa DOM, jak i sprawdzanie informacji w wyrenderowanych elementach.

Zwróć uwagę, że używamy tutaj selektora .title, podczas gdy w Hero.js znajduje się className={styles.title}. Jak zapewne pamiętasz, w kodzie strony to wyrażenie jest zamieniane na tekst w stylu Hero_title_19wwZ, ale przy testowaniu za pomocą Jesta będzie zamienione na title. Dzięki temu możemy używać selektorów analogicznych do tych z naszego kodu SCSS.

Dalsze testy

Zastanówmy się teraz nad propsem imageSrc – jeśli zawiera ścieżkę do pliku, to powinien wyrenderować się <img> z tą ścieżką. Ciężko by nam było sprawdzać, czy ścieżka do pliku jest poprawna, więc tego nie będziemy robić – ale co powinno się stać, jeśli w ogóle nie zostanie ona podana?

Aby sprawdzić, jak w tym momencie wyrenderuje się nasz komponent, wystarczy w pierwszym teście dodać:

console.log(component.debug());

Spójrz teraz na komunikat w terminalu.

image

Niedobrze – nasz komponent renderuje <img> z atrybutem src równym undefined. W tej sytuacji powinien występować błąd albo element <img> nie powinien się renderować. Dla uproszczenia przyjmijmy drugi wariant – w propTypes komponentu Hero ustaw props imageSrc jako wymagany (.isRequired).

Teraz jest lepiej – ale nasze testy przestały przechodzić! Musimy je poprawić! W pierwszym z nich musimy dodać props imageSrc o dowolnej wartości tekstowej, a trzeci test przerobimy tak, aby sprawdzał, czy obrazek otrzymuje poprawny adres z propsa:

it('should render correct title and image', () => {
  const expectedTitle = 'Lorem ipsum';
  const expectedImage = 'image.jpg';
  const component = shallow(<Hero titleText={expectedTitle} imageSrc={expectedImage} />);

  const renderedTitle = component.find('.title').text();
  expect(renderedTitle).toEqual(expectedTitle);
  expect(component.find('.image').prop('src')).toEqual(expectedImage);
});

Tym razem, zamiast tworzyć stałą renderedImage, bezpośrednio w funkcji expect wpisaliśmy wyrażenie, które znajduje element z klasą image i sprawdza jego props src. Możesz spotkać się z tym zapisem przy dość krótkich wyrażeniach, więc będziemy go niekiedy używać.

Na koniec dodajmy jeszcze jeden test – tym razem sprawdzimy, czy nasz wyrenderowany wrapper komponentu otrzymuje poprawne klasy, w zależności od propsa variant.

it('renders correct classNames', () => {
  const mockVariants = 'small dummy';
  const component = shallow(<Hero titleText='Lorem' imageSrc='image.jpg' variant={mockVariants} />);
  expect(component.hasClass('component')).toBe(true);
  expect(component.hasClass('small')).toBe(true);
  expect(component.hasClass('dummy')).toBe(true);
});

W tym teście nie sprawdzamy propsów titleText i imageSrc, więc podaliśmy im przykładowe wartości. W funkcjach expect wykorzystaliśmy tym razem metodę hasClass, którą również znajdziesz w dokumentacji Enzyme.

Funkcja shallow

Na koniec tego submodułu wrócimy jeszcze do funkcji shallow. Jak wspomnieliśmy wcześniej, renderuje ona komponent, który podajemy jako jej argument. Jest to jednak duży skrót myślowy – przecież w czasie testów strona nie wyświetla się w przeglądarce, więc niby gdzie renderuje się ten komponent?

Enzyme zawiera bibliotekę JSDOM, która symuluje drzewo DOM tworzone przez przeglądarkę. Pozwala nam to na niby-renderowanie komponentów do kodu... HTML? Właśnie nie HTML, tylko JSX! Spójrz dokładnie na kod komponentu Hero wyświetlony przez wyrażenie:

console.log(component.debug());

W terminalu widzimy taki kod:

<div className="component ">
  <h2 className="title">
    Lorem ipsum
  </h2>
  <img className="image" src="image.jpg" />
</div>

Zwróć uwagę na className zamiast class! Po tym możemy poznać, że jest to kod JSX, a nie HTML!

Co więcej, funkcja shallow renderuje tylko komponent, który jej przekazujemy, bez renderowania komponentów zawartych w nim. Zobaczymy to na przykładzie już niedługo, przechodząc do testowania innych komponentów.

Gdybyśmy potrzebowali wyrenderować wszystkie komponenty zawarte w testowanym komponencie, możemy użyć funkcji mount zamiast shallow – jednak w ramach tego modułu nie będziemy mieć takiej potrzeby. Ta funkcja ma większe zastosowanie w testach integracyjnych, a my zajmujemy się testami jednostkowymi.

Na razie poznaliśmy sposób korzystania z unit testing, ale to dopiero początek – już niedługo zajmiemy się testowaniem komponentu interaktywnego, czyli takiego, który nie tylko wyświetla coś na stronie, ale również reaguje na interakcję z użytkownikiem!

Zadanie: Testy kolejnego komponentu

To zadanie będzie się składało z dwóch części. Pierwsza z nich pozwoli Ci na utrwalenie zdobytej wiedzy o pisaniu testów jednostkowych, a druga część pozwoli Ci na lepsze zrozumienie ich działania.

Testy elementów komponentu TripSummary

Ten komponent jest wykorzystywany na stronie Trips do wyświetlania pojedynczej wycieczki. Twoim zadaniem jest napisanie testu w pliku TripSummary.test.js, który sprawdzi:

  • czy generowany jest link do poprawnego adresu, np. '/trip/abc', jeśli props id ma wartość 'abc',
  • czy <img> ma poprawne src i alt,
  • czy poprawnie renderują się propsy name, cost i days,
  • czy jest wywoływany błąd w przypadku braku któregokolwiek z propsów: id, image, name, cost i days.

Niektóre z tych testów mogą kończyć się wynikiem negatywnym – nie musi to jednak oznaczać, że test został napisany błędnie. Problem może leżeć w samym komponencie, który np. coś niepoprawnie renderuje, albo nie ma któregoś z propsów ustawionych jako wymagane.

Pamiętaj też, że jako selektor w metodzie .find możesz wykorzystać również nazwy tagów (np. article) oraz nazwy komponentów (np. Link). Używaj ich jednak tylko jeśli element nie ma klasy i możesz założyć, że jest to jedyny element tego typu w komponencie.

Testy tagów w TripSummary

Druga część zadania dotyczy przetestowania propsa tags w tym samym komponencie. Tu sprawa o tyle się komplikuje, że ten props powinien zawierać tablicę, zawierającą tagi. Dla każdego z nich powinien zostać wyrenderowany <span> zawierający nazwę tego tagu.

W tym wypadku przekaż komponentowi TripSummary props tags, którego wartością będzie tablica. Jej elementami mogą być dowolne teksty.

Napisz test, który będzie przekazywał komponentowi trzy tagi, i sprawdzi, czy są one renderowane w spanach w odpowiedniej kolejności. Z pewnością przyda Ci się informacja, że metoda .find znajduje wszystkie elementy pasujące do selektora, a konkretny z nich (np. drugi) możesz wybrać za pomocą metody .at (opisanej w dokumentacji).

Ostatni test do napisania w ramach tego zadania będzie bardzo prosty – jeśli props tags jest fałszywy (czyli np. nie został podany) lub jest pustą tablicą, to w ogóle nie powinien być renderowany div z klasą tags. Ten test początkowo nie będzie przechodził – dopiero po napisaniu testu wprowadź zmiany w kodzie JSX komponentu, aby uzyskać pozytywny wynik tego testu.

Powodzenia!

19.3. Testy interakcji

Czas na zajęcie się trochę bardziej skomplikowanym komponentem. Przetestujemy OrderOption oraz jego subkomponenty, np. OrderOptionDropdown. Zaczynamy standardowo – stwórz plik OrderOption.test.js w katalogu tego komponentu.

Podstawowe testy komponentu OrderOption

Pisanie testu zacznij od zaimportowania Reacta, funkcji shallow oraz komponentu OrderOption. Napisz też funkcję describe, a w niej test it sprawdzający, czy komponent się renderuje. Pamiętaj, że musisz przekazać temu komponentowi przynajmniej propsy type i name.

Test powinien dać wynik pozytywny – możemy więc przejść do drugiego testu. Tym razem sprawdzimy, czy przy braku podanego typu opcji komponent zachowa się poprawnie, czyli zwróci null. Osiągniemy to, porównując wyrenderowany komponent z pustym obiektem.

it('should return empty object if called without required props', () => {
  const component = shallow(<OrderOption />);
  expect(component).toEqual({});
});

Wreszcie, ostatni test sprawdzający sam komponent OrderOption – napisz samodzielnie test, który upewni się, że w tytule wyświetla się zawartość propsa name.

Schemat testowania subkomponentów

Wiemy, że OrderOption renderuje jeden ze swoich subkomponentów, a każdy z nich działa dość podobnie. Zgodnie z zasadą Don't repeat yourself, postaramy się uniknąć niepotrzebnego duplikowania kodu. Właśnie dlatego stworzymy pętlę, która pomoże nam testować każdy z subkomponentów.

Na końcu pliku dodaj następujący kod:

const optionTypes = {
  dropdown: 'OrderOptionDropdown',
  icons: 'OrderOptionIcons',
  checkboxes: 'OrderOptionCheckboxes',
  number: 'OrderOptionNumber',
  text: 'OrderOptionText',
  date: 'OrderOptionDate',
};

for(let type in optionTypes){
  describe(`Component OrderOption with type=${type}`, () => {
    /* test setup */

    /* common tests */
    it('passes dummy test', () => {
      expect(1).toBe(1);
    });

    /* type-specific tests */
    switch (type) {
      case 'dropdown': {
        /* tests for dropdown */
        break;
      }
    }
  });
}

Mamy tutaj obiekt zawierający wszystkie typy opcji oraz nazwy odpowiadających im subkomponentów. Następnie w pętli iterujemy po nich, zapisując typ opcji w zmiennej type.

Możesz teraz uruchomić testy – mamy tylko jeden przykładowy test, który zawsze zwróci prawdę (bo sprawdza tylko, czy 1 jest równe 1). Zauważ jednak, że ten test uruchomi się dla każdego typu opcji, którego nazwa będzie podana w opisie grupy testów (podanym w funkcji describe).

Przygotowanie do testów

Wewnątrz pętli wykorzystujemy describe do stworzenia nowego pakietu testów, którego opis zawiera typ, po którym aktualnie iterujemy.

W describe za pomocą komentarzy wydzieliliśmy trzy sekcje:

  1. test setup, którym zajmiemy się za chwilę,
  2. common tests, w którym będziemy pisać testy dotyczące każdego subkomponentu,
  3. type-specific tests, gdzie znajdziesz wyrażenie switch – w nim będziemy pisać testy dla konkretnych typów opcji.

Ta ostatnia sekcja może wydawać Ci się dziwna – po co używamy pętli, skoro potem i tak będziemy pisać osobne testy dla każdego typu? Sekret leży w sekcji test setup! Dodamy w niej operacje, które będą wykonywane przed każdym testem – dzięki temu nie będziemy musieli się powtarzać!

W sekcji test setup wstaw następujący kod:

let component;

beforeEach(() => {
  component = shallow(
    <OrderOption
      type={type}
    />
  );
});

Funkcja beforeEach wykona się przed uruchomieniem każdego z testów it – oznacza to, że każdy test będzie miał do dyspozycji świeżo wyrenderowany komponent OrderOption, i nie musimy używać funkcji shallow w każdym z testów!

Możemy od razu to sprawdzić, wpisując w naszym przykładowym teście o nazwie "passes dummy test" znany już Ci console.log:

console.log(component.debug());

Jeśli wszystko poszło dobrze, test wyświetli w terminalu kod JSX każdego z wyrenderowanych komponentów. Zwróć uwagę, że w każdym z nich znalazł się inny subkomponent!

Renderowanie subkomponentów

Do przetestowania np. OptionOrderDropdown nie wystarczy nam jednak informacja, że OptionOrder go wykorzystuje. Musimy wyrenderować również ten subkomponent. Chcemy jednak upewnić się, że testujemy tylko ten jeden subkomponent – jeśli OptionOrder zawiera jakiekolwiek inne (np. Col czy Icon), chcemy, aby pozostały jako kod JSX. Dlatego nie użyjemy mount (o którym wspominaliśmy wcześniej) zamiast shallow – lepiej będzie skorzystać z metody .dive!

Zmień kod w sekcji test setup na następujący:

let component;
let subcomponent;
let renderedSubcomponent;

beforeEach(() => {
  component = shallow(
    <OrderOption
      type={type}
    />
  );
  subcomponent = component.find(optionTypes[type]);
  renderedSubcomponent = subcomponent.dive();
});

Nie przejmuj się, że testy zaczną sypać błędami – wręcz możesz się z tego ucieszyć! Dzieje się tak, ponieważ metoda .dive wyrenderowała subkomponenty, i to one zgłaszają błędy, ponieważ nie otrzymały propsów wymaganych do poprawnego działania.

Zanim to naprawimy, zwróć uwagę, że musieliśmy najpierw w wyrenderowanym komponencie OrderOption znaleźć subkomponent za pomocą metody .find. Jako selektora użyliśmy w tym wypadku po prostu nazwy subkomponentu.

Dodanie mockowanych propsów

Angielskie słowo mock na stałe zagości już w Twoim słowniku. Używamy go w znaczeniu, które tłumaczy się jako atrapa. W testach jednostkowych będziemy używać wielu atrap i już to nie raz zrobiliśmy – za każdym razem, kiedy podawaliśmy komponentowi jakieś propsy. Nie podajemy mu wtedy prawdziwych danych, z których korzysta nasza aplikacja, tylko jakieś przykładowe, stanowiące właśnie atrapę.

Tym razem pójdziemy o krok dalej i przygotujemy sobie cały obiekt zawierający wiele propsów, które chcemy nadawać subkomponentom. Jest to mieszanka właściwości różnych opcji z pliku pricing.json. Wstaw ten kod przed pętlą for:

const mockProps = {
  id: 'abc',
  name: 'Lorem',
  values: [
    {id: 'aaa', icon: 'h-square', name: 'Lorem A', price: 0},
    {id: 'xyz', icon: 'h-square', name: 'Lorem X', price: 100},
  ],
  required: false,
  currentValue: 'aaa',
  price: '50%',
  limits: {
    min: 0,
    max: 6,
  },
};

const mockPropsForType = {
  dropdown: {},
  icons: {},
  checkboxes: {currentValue: [mockProps.currentValue]},
  number: {currentValue: 1},
  text: {},
  date: {},
};

const testValue = mockProps.values[1].id;
const testValueNumber = 3;

W pliku pricing.json możesz sprawdzić, że tylko niektóre opcje mają właściwość values, a inne limits. My złączyliśmy wszystkie możliwości w jednym obiekcie mockProps, aby ułatwić sobie testowanie. Subkomponenty i tak będą korzystać tylko z niektórych propsów, więc nie musimy się tym przejmować.

Następny obiekt, mockPropsForType, zawiera propsy istotne tylko dla konkretnego typu opcji. Na przykład, OrderOptionCheckboxes wymaga, aby currentValue było tablicą, a number – liczbą.

Na końcu mamy dwie stałe, testValue i testValueNumber – będziemy się starali, aby każdy subkomponent przyjął właśnie tę wartość. Innymi słowy, ta wartość to nasz cel, do którego dążymy. Zwróć uwagę, że testValue odwołuje się do id drugiego obiektu w mockProps.values, podczas gdy mockProps.currentValue jest równe id pierwszego obiektu. W ten sposób zasymulujemy sytuację, w której opcja ma już jakąś wartość, którą chcemy zmienić na inną (lub do której dodamy inną, w przypadku checkboxes).

Teraz musimy wykorzystać te atrapy propsów w funkcji beforeEach. Pod propsem type w tagu <OrderOption /> dodaj:

{...mockProps}
{...mockPropsForType[type]}

Po tej zmianie nasz test "passes dummy test" powinien ponownie działać dla każdego typu opcji. Możesz dodać do niego drugi console.log, tym razem wyświetlający subcomponent.debug() (czyli subkomponent, a nie komponent). Sprawdź wynik w terminalu, aby zobaczyć, że faktycznie subkomponenty zostały wyrenderowane.

Wspólne testy subkomponentów

Czas zmienić ten przykładowy test na nieco bardziej przydatny:

it(`renders ${optionTypes[type]}`, () => {
  expect(subcomponent).toBeTruthy();
  expect(subcomponent.length).toBe(1);
});

Podobnie jak wcześniej dla komponentu, tak teraz dla każdego subkomponentu sprawdzamy, czy w ogóle się renderuje. Gdyby np. nasza struktura kodu wymagała, aby każdy z nich zawierał Col, moglibyśmy również to sprawdzić w sekcji common tests.

W naszym przypadku nie ma takiej potrzeby, więc możemy przejść do ostatniej sekcji – type-specific tests.

Testy dla dropdownów

W miejscu komentarza /* tests for dropdown */ wstawimy dwa testy. Pierwszy z nich sprawdzi, czy ten subkomponent zawiera odpowiednie elementy, czyli <select> i <option>. Szybkie spojrzenie do pliku OrderOptionDropdown.js przypomni Ci, że jeśli props required jest fałszywy (a w naszych mockowanych propsach ma wartość false), zostanie dodany dodatkowy <option> z pustą wartością. Sprawdzimy więc zarówno obecność selecta, opcji z pustą wartością, jak i po jednej opcji dla każdego obiektu z mockProps.values.

it('contains select and options', () => {
  const select = renderedSubcomponent.find('select');
  expect(select.length).toBe(1);

  const emptyOption = select.find('option[value=""]').length;
  expect(emptyOption).toBe(1);

  const options = select.find('option').not('[value=""]');
  expect(options.length).toBe(mockProps.values.length);
  expect(options.at(0).prop('value')).toBe(mockProps.values[0].id);
  expect(options.at(1).prop('value')).toBe(mockProps.values[1].id);
});

Zwróć uwagę, że w stałej emptyOption używamy selektora atrybutu, a w stałej options – metody .not. W ten sposób możemy wybrać, które opcje chcemy zapisać w każdej z tych stałych.

Test interaktywności

Wspominaliśmy o dwóch testach – drugi z nich zajmie się sprawdzeniem interaktywności tego subkomponentu!

Przypomnijmy najpierw, jak działa jego interaktywność. Komponent OrderOption otrzymuje propsa setOrderOption. Jest to funkcja, która ma otrzymywać obiekt w formacie {idOpcji: wartośćOpcji}. Ten komponent przekazuje subkomponentowi w propsie setOptionValue inną funkcję:

value => setOrderOption({[id]: value})

Każdy z subkomponentów na swój sposób wykorzystuje tę funkcję, zawartą w propsie setOptionValue – może uruchamiać ją przy różnych eventach i przekazywać jej różne wartości. W przypadku OrderOptionDropdown będzie to event change, a wartością będzie właściwość value obiektu zapisanego w event.currentTarget.

Aby poprawnie sprawdzić działanie tego subkomponentu, musimy zasymulować event change oraz zawartość event.currentTarget, aby sprawdzić, czy wtedy subkomponent w reakcji na ten event wykona funkcję setOptionValue, która wykona setOrderOption.

Mockowanie funkcji

Chwileczkę – skąd mamy wiedzieć, czy wywołana została ta funkcja? Jak to sprawdzić? Komponent OrderOption otrzymuje ją jako propsa, więc możemy mu przekazać dowolną funkcję! Co więcej, Jest posiada wbudowany mechanizm do mockowania funkcji, czyli budowania atrapy, która pozwoli nam sprawdzić, czy ta funkcja była wykonana, ile razy, z jakimi argumentami, etc.

Znajdź ten fragment kodu:

let renderedSubcomponent;

beforeEach(() => {
  component = shallow(
    <OrderOption
      type={type}
      {...mockProps}
      {...mockPropsForType[type]}
    />
  );

Zmień go, dodając 3 linie kodu, które oznaczyliśmy komentarzami poniżej:

let renderedSubcomponent;
let mockSetOrderOption; /* 1 */

beforeEach(() => {
  mockSetOrderOption = jest.fn(); /* 2 */
  component = shallow(
    <OrderOption
      type={type}
      setOrderOption={mockSetOrderOption} /* 3 */
      {...mockProps}
      {...mockPropsForType[type]}
    />
  );

Pierwszą i ostatnią na pewno zrozumiesz, ale najciekawsza jest druga – wyrażenie jest.fn() to właśnie sposób na stworzenie atrapy funkcji! Za chwilę zobaczysz, jak będziemy ją wykorzystywać!

Symulowanie eventu i wykorzystanie atrapy funkcji

Dodaj kolejny test dla dropdowna, wewnątrz wyrażenia switch:

it('should run setOrderOption function on change', () => {
  renderedSubcomponent.find('select').simulate('change', {currentTarget: {value: testValue}});
  expect(mockSetOrderOption).toBeCalledTimes(1);
  expect(mockSetOrderOption).toBeCalledWith({ [mockProps.id]: testValue });
});

Mamy tutaj trochę nowości, więc omówmy je po kolei. Po znalezieniu selecta wykonujemy na nim metodę .simulate, która przyjmuje jeden lub dwa argumenty. Pierwszym z nich jest rodzaj eventu, jaki ma zostać zasymulowany – w tym wypadku event change. Drugi argument to wartość przekazywana handlerowi tego eventu. Jak zwykle, handler eventu spodziewa się, że otrzyma obiekt event, ale nie musimy mockować całej jego zawartości – ten handler korzysta tylko z właściwości currentTarget, z której pobiera value. Dlatego właśnie jako drugi argument podaliśmy taką atrapę obiektu event.

Przechodząc do kolejnej linii kodu, sprawdzamy, czy ta funkcja została wykonana dokładnie jeden raz. Natomiast w ostatniej linii kodu sprawdzamy, czy została wywołana z poprawnymi argumentami.

Zmienna jako klucz w obiekcie

Warto przypomnieć, co oznaczają nawiasy kwadratowe w powyższym wyrażeniu. Przeanalizuj ten przykład:

const keyName = 'car-rental';

const options = {
  [keyName]: 'SUV',
};

console.log(options);

/*
  {
    car-rental: 'SUV'
  }
*/

W komentarzu przedstawiliśmy wynik, który zostałby wyświetlony w konsoli za pomocą console.log. Nawiasy kwadratowe pozwoliły nam wykorzystać wartość zmiennej jako klucz w obiekcie. Bez tego zapisu musielibyśmy zrobić to nieco naokoło:

const keyName = 'car-rental';

const options = {};
options[keyName] = 'SUV';

console.log(options);

W obu przypadkach rezultat będzie ten sam, ale to ten pierwszy zapis będzie dla nas najwygodniejszy – nie wymaga on zapisania zmiennej, czyli moglibyśmy z tym samym efektem napisać:

const keyName = 'car-rental';

console.log({
  [keyName]: 'SUV',
});

Sprawdzając dokumentację Jesta nie zdziw się, że wykorzystane przez nas metody widnieją pod nazwami .toHaveBeenCalledTimes i .toHaveBeenCalledWith – w naszym kodzie użyliśmy ich aliasów, ponieważ są krótsze i bardziej czytelne.

Podsumowanie testów interakcji

Ten submoduł mógł być wyzwaniem dla Ciebie, ponieważ wykorzystuje nieco skomplikowany algorytm testowania wielu subkomponentów. Była to dla Ciebie okazja do treningu swojego rozumienia algorytmicznego. ;)

Jednak całe zagadnienie testu interaktywności zawarliśmy w powyższym rozdziale – wystarczyło stworzyć atrapę funkcji za pomocą jest.fn(), zasymulować event za pomocą .simulate (podając obiekt oczekiwany przez handler eventu), oraz sprawdzić, co przechwyciła atrapa funkcji za pomocą metod .toBeCalledTimes i .toBeCalledWith.

I to jest klucz do testów jednostkowych sprawdzających działanie interakcji – symulujemy event i sprawdzamy, czy wykonała się atrapa funkcji, podana w propsach.

Zadanie: Dokończenie testów subkomponentów

Jak już zapewne się domyślasz, Twoim zadaniem będzie napisanie testów dla pozostałych subkomponentów. W wyrażeniu switch dodaj sekcję case dla każdego typu opcji (czyli icons, checkboxes, etc.).

Dla każdego typu opcji wykonaj dwa testy, analogicznie do tych napisanych w case 'dropdown' – jeden test sprawdzający, czy renderuje odpowiednie elementy (np. divy z klasą icon, inputy z type="checkbox", etc.), oraz drugi test, który powinien sprawdzać interakcję, czyli symulować event i korzystać z atrapy funkcji.

Z testami sprawdzającymi renderowanie elementów z pewnością poradzisz już sobie bez podpowiedzi, ale do testów interakcji przygotowaliśmy kilka wskazówek.

Interakcje dla icons

Na ostatnim divie z klasą icon zasymuluj kliknięcie. Drugi argument w simulate nie będzie potrzebny.

Interakcje dla checkboxes

Tu mamy nieco bardziej skomplikowaną sytuację – musisz znaleźć element, który ma atrybut value o wartości takiej samej, jak wartość stałej testValue. Na tym elemencie należy zasymulować event change, ale w drugim argumencie, zamiast value, podać checked: true. Dzięki temu handler eventu będzie myślał, że ten checkbox został zaznaczony.

Przy sprawdzaniu, czy atrapa funkcji została wykonana z odpowiednimi argumentami, musisz wziąć poprawkę na dwie rzeczy:

  • w przeciwieństwie do pozostałych typów opcji, w przypadku checkboxes wartością opcji jest tablica,
  • stan początkowy, zdefiniowany w mockPropsForType dla checkboxes, zawiera już mockProps.currentValue.

W związku z tym, tam, gdzie w poprzednich typach opcji spodziewaliśmy się wartości testValue, tym razem będzie tablica zawierająca najpierw mockProps.currentValue, a potem testValue.

Interakcje dla number

Test będzie bardzo podobny jak dla dropdown, z tym że:

  • szukamy inputa, a nie selecta,
  • zamiast testValue stosujemy testValueNumber.

Interakcje dla text

Jeszcze bardziej podobnie do dropdown – jedyna różnica to szukanie inputa zamiast selecta.

Interakcje dla date

W tym wypadku nie mamy inputa czy selecta, ale komponent DatePicker i to właśnie jego będziemy szukać (find(Datepicker)). Komponent musimy zaimportować, nie ma potrzeby jednak go renderować – wystarczy, że zasymulujemy na nim event change, a jako drugi argument podamy testValue zamiast obiektu, który do tej pory wstawialiśmy w to miejsce.

Podsumowanie zadania

Z tymi wskazówkami napisanie testów do każdego z subkomponentów powinno się udać bez większych problemów. Po wykonaniu zadania wyślij je do Mentora.

19.4. Wysyłanie zamówienia do API

Teraz kiedy dodaliśmy już testy dla naszej aplikacji agencji turystycznej, możemy dokończyć funkcjonalność zamawiania wycieczki. W tym celu znów skorzystamy z json-server, którego używaliśmy już przy projekcie pizzerii.

Jak zapewne pamiętasz, w tym projekcie zdecydowaliśmy się przechowywać wszystkie informacje w plikach JSON. Dlatego API posłuży nam tylko do przyjmowania zamówień – czy raczej chęci zamówienia wycieczki, bo jak już wspominaliśmy, nasz formularz zamówienia jest de facto rozbudowanym formularzem kontaktowym.

Konfiguracja json-server

Nasze API będzie potrzebowało pliku JSON, który będzie służył za bazę danych. Pamiętaj, że to API nie jest w żaden sposób zabezpieczone i służy nam wyłącznie do testowania aplikacji reactowej.

Zaczynamy od zainstalowania json-server, który za chwilę wykorzystamy.

npm install -S json-server

Następnie pobieramy paczkę z plikami:

Pobierz paczkę plików

W tej paczce znajdziesz pliki:

  • server.js i Procfile, które należy umieścić w głównym katalogu projektu,
  • db.json, który należy umieścić w folderze src.

Ostatnim krokiem uruchomienia API będzie zmiana tego fragmentu package.json:

"start": "webpack-dev-server --mode development --open --hot",

na następujący:

"start": "npm-run-all -p server:*",
"server:api": "node server.js",
"server:dev": "webpack-dev-server --mode development --open --hot",

Zanim omówimy te zmiany, zainstalujmy jeszcze pakiet npm-run-all, który będziemy wykorzystywać w naszym środowisku developerskim.

npm install -D npm-run-all

Pakiet npm-run-all pozwoli nam na równoległe uruchomienie:

  • komendy node server.js, która uruchomi json-server za pomocą pliku server.js,
  • taska server:dev, który uruchamia podgląd developerski (webpack-dev-server), tak samo, jak do tej pory.

Jednoczesne działanie obu tych serwerów będzie nam potrzebne wyłącznie przy pracy lokalnej. Nie musimy się przejmować serwerem, ponieważ na nim nie będziemy uruchamiać webpack-dev-server. Serwowaniem naszej aplikacji zajmie się json-server, podobnie jak miało to miejsce przy projekcie pizzerii.

Plik server.js

We wcześniejszym projekcie ten plik służył wyłącznie do uruchomienia json-server, który jednocześnie pełnił rolę API (w oparciu o plik JSON), jak i zajmował się serwowaniem plików strony.

Tym razem dodaliśmy do niego funkcjonalność kopiowania pliku src/db.json do katalogu server – ale tylko jeśli ten plik jeszcze nie istnieje. Dzięki temu przy kolejnych buildach nie będziemy nadpisywać jego zawartości.

Plik z ustawieniami

W katalogu src/data stwórz plik settings.js i umieść w nim następujący kod:

const settings = {
  db: {
    url: '//' + window.location.hostname + (window.location.hostname=='localhost' ? ':3131' : ''),
    endpoint: {
      orders: 'orders',
    },
  },
};

export default settings;

Będzie to zalążek ustawień projektu. Na razie najważniejszą pozycją jest właściwość url, która stworzy odpowiedni adres, rozpoznając, czy uruchomiliśmy projekt w środowisku developerskim, czy na serwerze.

Zmiany w komponencie OrderForm

W kodzie JSX tego komponentu dodaj guzik, który przy kliknięciu będzie wywoływał funkcję sendOrder, którą zaraz uzupełnimy.

<Button onClick={() => sendOrder(options, tripCost)}>Order now!</Button>

Następnie, pomiędzy importami a kodem komponentu OrderForm, wstaw tę funkcję:

const sendOrder = (options, tripCost) => {
  const totalCost = formatPrice(calculateTotal(tripCost, options));

  const payload = {
    ...options,
    totalCost,
  };

  const url = settings.db.url + '/' + settings.db.endpoint.orders;

  const fetchOptions = {
    cache: 'no-cache',
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  };

  fetch(url, fetchOptions)
    .then(function(response){
      return response.json();
    }).then(function(parsedResponse){
      console.log('parsedResponse', parsedResponse);
    });
};

Kiedy samodzielnie uzupełnisz wszystkie niezbędne importy, kliknięcie guzika będzie wysyłało zamówienie do serwera. Możesz sprawdzić zawartość API pod adresem http://localhost:3131/db.

Zadanie: Skompletowanie i walidacja danych

Szybko możesz zauważyć, że dane wysyłane do API nie są kompletne. Brakuje w nich na przykład nazwy i id wycieczki. Również kod kraju mógłby być przydatny. Poza tym nie powinniśmy umożliwiać wysłania zamówienia, jeśli pola name i contact są puste.

Właśnie te funkcjonalności są Twoim zadaniem w tym submodule! Powodzenia!

19.5. Podsumowanie

Na tym kończy się nasza wspólna przygoda z tym projektem – gorąco zachęcamy Cię do samodzielnej kontynuacji pracy nad nim!

W następnym module przejdziemy do rozpoczęcia zupełnie nowego projektu, który będzie zwieńczeniem Twojej nauki w ramach kursu. Skorzystamy w nim z popularnego pakietu create-react-app, aby usprawnić start projektu.

19.6. Quiz powtórkowy

Na koniec tego modułu przygotowaliśmy dla Ciebie quiz powtórkowy. Pomoże Ci on powtórzyć wiedzę z poprzednich modułów.

Odpowiedzi tego quizu nie są nigdzie zapisywane, więc są tylko do Twojej wiadomości. Ten quiz ma Ci posłużyć jako pomoc w nauce – dlatego pod każdym pytaniem znajdziesz guzik, który sprawdzi poprawność Twoich odpowiedzi oraz poda Ci wyjaśnienie zagadnienia poruszanego w tym pytaniu.

1. Poniższa funkcja strzałkowa może być wykorzystywana do sumowania wyników na stronie egzaminu. Jako argument otrzymuje tablicę z liczbą punktów za każde pytanie. Funkcja ma zwracać sumę punktów.

const getResult = scores => {
  let sum = 0;

  for(let i=0; i<scores.length; i++){
    /* missing code */
  }

  return sum;
};

Przykładem wykorzystania tej funkcji może być np.:

const result = getResult([7, 3, 6, 2, 2, 4]);
console.log(result);

Jak widzisz, w funkcji brakuje jednej linii kodu, oznaczonej komentarzem /* missing code */. Wybierz te fragmenty kodu, które moglibyśmy wykorzystać w miejscu tego komentarza, aby funkcja poprawnie sumowała punkty z tablicy scores.

Wyjaśnienie

Zadaniem tej funkcji jest obliczenie sumy liczb otrzymanych w argumencie, który nazwaliśmy scores. W związku z tym wykorzystaliśmy pętlę for, która wykona się tyle razy, ile mamy elementów w tablicy scores.

Skoro w każdej iteracji mamy dodać kolejną liczbę do sumy, to musimy pobierać kolejne wartości z tablicy scores. Pomoże nam w tym iterator pętli, czyli i, które będzie przyjmowało wartości od 0 do ostatniego indeksu elementu w tablicy.

Jak zapewne pamiętasz, kiedy indeks elementu tablicy mamy zapisany w zmiennej, używamy składni:

nazwaTablicy[indeksElementu]

Czyli w naszym przypadku będzie to:

scores[i]

W takim razie pozostaje dodanie tej wartości do zmiennej sum. W tym celu możemy wykorzystać składnię:

sum = sum + scores[i];

Alternatywnie, zamiast sum = sum + możemy napisać sum +=, otrzymując krótszy zapis:

sum += scores[i];

2. Co będzie wartością zmiennej message po wykonaniu tego kodu:

const message = 100 + 25 + ' studentów zapisało się na ' + 2 + 7 + ' wykładów';

Wyjaśnienie

W JavaScripcie operator + pełni jednocześnie rolę sumowania liczb, jak i łączenia tekstów. Wybór jednej z tych ról jest podejmowany automatycznie w oparciu o to, co znajduje się po obu stronach tego operatora. Innymi słowy, tylko jeśli po obu stronach będą liczby, zostanie wykonane dodawanie.

Podobnie jak w matematyce, wykonujemy operacje dodawania (lub łączenia tekstów) od lewej strony, czyli zaczynamy od 100 + 25. Z obu stron są liczby, więc otrzymamy sumę 125.

Następny krok to 125 + ' studentów... ' – tym razem pojawił się tekst, więc w rezultacie otrzymamy złączony tekst '125 studentów... '.

Wszystkie kolejne operatory + będą miały po lewej stronie tekst, więc będą wykonywały złączenie tekstów. W związku najpierw zostanie dodana do niego liczba 2, potem liczba 7, a potem tekst ' wykładów'.

W rezultacie otrzymamy tekst '125 studentów zapisało się na 27 wykładów'.

Aby liczby 2 i 7 zostały zsumowane, moglibyśmy zapisać ich sumę w osobnej zmiennej, ale jest też prostsze rozwiązanie – nawiasy ( ).

const message = 100 + 25 + ' studentów zapisało się na ' + (2 + 7) + ' wykładów';

Wyrażenie w nawiasach zostanie wykonane, zanim zostaną połączone teksty. W ten sposób otrzymamy tekst: '125 studentów zapisało się na 9 wykładów'.

3. Do kodu strony został dodany taki fragment HTML-a:

<div class="wrapper">
  <a href="index.html" class="card">
    <img src="images/icons/plus.png">
    <div class="description">
      Add new <strong>task</strong>
    </div>
  </a>
</div>

Jedyny kod JS uruchamiany na tej stronie to:

document.querySelector('.wrapper').addEventListener('click', function(event){
  console.log('wrapper');
  event.preventDefault();
});
document.querySelector('.card').addEventListener('click', function(event){
  console.log('card');
  event.preventDefault();
});
document.querySelector('.description').addEventListener('click', function(event){
  console.log('description');
  event.stopPropagation();
});
document.querySelector('strong').addEventListener('click', function(event){
  console.log('strong 1');
  event.stopImmediatePropagation();
});
document.querySelector('strong').addEventListener('click', function(event){
  console.log('strong 2');
  event.preventDefault();
  event.stopPropagation();
  event.stopImmediatePropagation();
});

Które komunikaty zostaną wyświetlone w konsoli po kliknięciu w słowo "task"?

Wyjaśnienie

To zadanie sprawdza Twoją znajomość metod eventu, wykorzystywanych do kontroli reakcji przeglądarki na ten event:

  • event.preventDefault(); powstrzymuje wykonanie domyślnej akcji – np. przy kliknięciu w link domyślną akcją jest przejście pod adres znajdujący się w href tego linka,
  • event.stopPropagation(); powstrzymuje tzw. bąbelkowanie (ang. bubbling), czyli przekazywanie eventu rodzicowi danego elementu,
  • event.stopImmediatePropagation(); blokuje działanie listenerów eventu dla tego elementu, które zostały zdefiniowane później niż aktualnie wykonywany listener.

Przeanalizujmy działanie powyższego kodu JS. Kliknięte zostało słowo "task", czyli element strong. Temu elementowi zostały przypisane dwa listenery eventów. Pierwszy z nich wykonuje event.stopImmediatePropagation();, przez co drugi nie zostanie wykonany. Stąd wiemy już, że w konsoli wyświetli się komunikat strong 1, ale nie wyświetli się strong 2.

Następnie event click zostanie propagowany do rodzica, czyli wykona się na divie z klasą description. W handlerze eventu dla tego elementu wykonaliśmy event.stopImmediatePropagation();, więc event nie zostanie przekazany dalej w górę drzewa DOM. Dlatego komunikat description będzie ostatnim, który wyświetli się w konsoli.

;